/*
 * JBoss, Home of Professional Open Source
 *
 * Copyright 2008, Red Hat Middleware LLC, and individual contributors
 * by the @author tags. See the COPYRIGHT.txt in the distribution for a
 * full listing of individual contributors.
 *
 * This is free software; you can redistribute it and/or modify it
 * under the terms of the GNU Lesser General Public License as
 * published by the Free Software Foundation; either version 2.1 of
 * the License, or (at your option) any later version.
 *
 * This software is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this software; if not, write to the Free
 * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
 * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
 */
package org.jboss.netty.handler.codec.bayeux;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Stack;

/**
 * Parse JSON string to Java Object following mappings below:
 *
 * JSON => Java:
 *   object => Map
 *   array => Object[]
 *   string => String
 *   number => long, or double
 *   null => null
 *   boolean => boolean
 *
 * And convert from Java Object to JSON types following this:
 *
 * Java => JSON:
 *   Map => object,
 *   List => array,
 *   Array => array,
 *   Integer, Long, Double => number,
 *   int, long, double => number,
 *   null => null,
 *   boolean, Boolean=> boolean
 *   String => string
 *
 * @author daijun
 */
public class JSONParser {

    private JSONString json;//Reprents the JSON string, which is been parsing
    private Stack<TOKEN> tokens = new Stack();// Current state of a parser

    private enum TOKEN {//States of a parser

        PARSE_BEGIN,
        PARSE_END,
        COMMENT_BEGIN,
        COMMENT_END,
        ARRAY_BEGIN,
        ARRAY_END,
        OBJECT_BEGIN,
        OBJECT_END,
        VALUE_BEGIN,
        VALUE_END,
        STRING_BEGIN,
        STRING_END,
        NUMBER_BEGIN,
        NUMBER_END,
    }

    public JSONParser() {
        tokens.add(TOKEN.PARSE_BEGIN);
    }

    /**
     * Validate and parse a string to a JSON object. If succesful it will 
     * returns a Map or Array object, otherwise, it returns null. In below,
     * there are data types mappings JSON to Java.
     * 
     * JSON => Java:
     * object => Map
     * array => Object[]
     * string => String
     * number => long, or double
     * null => null
     * boolean => boolean
     *
     * @param s
     * @return
     * @throws java.lang.IllegalStateException
     */
    public Object parse(String s) throws IllegalStateException {
        if (s == null || s.trim().length() == 0) {
            return null;
        }

        json = new JSONString(s);
        while (json.hasNext()) {
            switch (json.next()) {
                case '/':
                    if (json.hasNext() && (json.next() == '/' || json.current() == '*')) {
                        skipComment();
                    } else {
                        throwIllegalJSONStatementException();
                    }
                    skipWhitespace();
                    break;
                case '{':
                    tokens.add(TOKEN.OBJECT_BEGIN);
                    return parseToObject();
                case '[':
                    tokens.add(TOKEN.ARRAY_BEGIN);
                    return parseToArray();
                default:
                    throwIllegalJSONStatementException();
                    break;
            }
        }
        return null;
    }

    /**
     *
     * @param obj
     * @return
     * @throws Exception
     */
    static public String toJSON(Object obj) {
        StringBuilder sb = new StringBuilder();
        if (obj == null) {
            sb.append("null");
        } else if (obj instanceof Map) {
            sb.append("{");
            Map<String, Object> map = (Map<String, Object>) obj;
            for (Entry<String, Object> entry : map.entrySet()) {
                sb.append(quote(entry.getKey())).append(":").append(toJSON(entry.getValue())).append(",");
            }
            sb.deleteCharAt(sb.length() - 1);
            sb.append("}");
        } else if (obj instanceof Boolean) {
            Boolean bool = (Boolean) obj;
            if (bool) {
                sb.append("true");
            } else {
                sb.append("false");
            }
        } else if (obj instanceof Long) {
            sb.append((Long) obj);
        } else if (obj instanceof Integer) {
            sb.append((Integer) obj);
        } else if (obj instanceof String) {
            sb.append(quote((String) obj));
        } else if (obj instanceof List) {
            sb.append("[");
            for (Object o : (List) obj) {
                sb.append(toJSON(o)).append(",");
            }
            sb.deleteCharAt(sb.length() - 1);
            sb.append("]");
        } else if (obj instanceof Object[]) {
            sb.append("[");
            for (Object o : (Object[]) obj) {
                sb.append(toJSON(o)).append(",");
            }
            sb.deleteCharAt(sb.length() - 1);
            sb.append("]");
        }
        return sb.toString();
    }

    static private String quote(String s) {
        return "\"" + s + "\"";
    }

    /**
     * Throw illegal JSON statement exception during parsing
     * 
     * When detects an unknown(or unvalid) char, it throws a IllegalStateException with
     * a message that where and what the char is.
     */
    private void throwIllegalJSONStatementException() {
        throw new IllegalStateException("Unknown char '" + json.current() + "' at position: " + json.getIndex());

    }

    /**
     * Handle JSON comment
     *
     * Skip the comments in the head of a JSON string. It supports two types
     * comments, \"//coments\" and \"/*coments*\/\"
     */
    private void skipComment() {
        if (json.current() == '/') {
            while (json.hasNext()) {
                if (json.next() == '\n') {
                    break;
                }
            }
        } else if (json.current() == '*') {
            while (json.hasNext()) {
                if (json.next() == '*' && json.next() == '/') {
                    break;
                }
            }
        }
    }

    /**
     * Skip white spaces inside the JSON string
     */
    private void skipWhitespace() {
        while (json.hasNext()) {
            if (json.next() != ' ') {
                json.back();
                break;
            }
        }
    }

    /**
     * Skip white spaces and determine whether the first non-white character is
     * expected
     * 
     * @param c
     * @return
     */
    private boolean skipWhitespace(char c) {
        skipWhitespace();
        return (json.hasNext() && json.next() == c);
    }

    /**
     * Parse string to a object
     *
     * Current character is \"{\", it means next serveral characters imply a
     * JSON object. According to mappings before, them should be parsed to a
     * Java object.
     *
     * @return
     */
    private Map parseToObject() {
        skipWhitespace();
        Map map = new HashMap();
        while (json.hasNext()) {
            switch (json.next()) {
                case '}':
                    return map.isEmpty() ? null : map;
                case '"':
                    tokens.add(TOKEN.STRING_BEGIN);
                    String key = parseToString();
                    if (skipWhitespace(':')) {
                        tokens.add(TOKEN.VALUE_BEGIN);
                        Object value = parseValue();
                        map.put(key, value);
                    } else {
                        throwIllegalJSONStatementException();
                    }
                    break;
                case ',':
                    skipWhitespace();
                    break;
                default:
                    throwIllegalJSONStatementException();
            }
        }
        return map;
    }

    /**
     * Parse string to a array
     *
     * Current character is \"[\", it means next serveral characters imply a
     * JSON array. According to mappings before, them should be parsed to a
     * Java array.
     * 
     * @return
     */
    private Object[] parseToArray() {
        skipWhitespace();
        ArrayList list = new ArrayList();
        while (json.hasNext()) {
            switch (json.next()) {
                case ']':
                    return list.isEmpty() ? null : list.toArray();
                case ',':
                    skipWhitespace();
                    break;
                default:
                    json.back();
                    list.add(parseValue());
                    break;
            }
        }
        return null;
    }

    /**
     * Parse a JSON value
     *
     * Any element of JSON array, or a property value of a JSON object, it maybe
     * a JSON value. So according to mappings before, them should be parsed to a
     * Java array.
     * 
     * @return
     */
    private Object parseValue() {
        skipWhitespace();
        while (json.hasNext()) {
            switch (json.next()) {
                case '"':
                    return parseToString();
                case '{':
                    return parseToObject();
                case '[':
                    return parseToArray();
                case 't':
                case 'T':
                    json.next();
                    json.next();
                    json.next();
                    return true;
                case 'f':
                case 'F':
                    json.next();
                    json.next();
                    json.next();
                    json.next();
                    return false;
                case 'n':
                case 'N':
                    json.next();
                    json.next();
                    json.next();
                    return null;
                case '0':
                case '1':
                case '2':
                case '3':
                case '4':
                case '5':
                case '6':
                case '7':
                case '8':
                case '9':
                    return parseToNumber();
                default:
                    throwIllegalJSONStatementException();
            }
        }
        return null;
    }

    /**
     * Parse string to a string
     *
     * Current character is \"\\"\", it means next serveral characters imply a
     * JSON string. According to mappings before, them should be parsed to a
     * Java string.
     * 
     * @return
     */
    private String parseToString() {
        skipWhitespace();
        StringBuilder sb = new StringBuilder();
        while (json.hasNext()) {
            switch (json.next()) {
                case '"':
                    return sb.toString();
                default:
                    sb.append(json.current());
                    break;
            }
        }
        return sb.toString();
    }

    /**
     * Parse string to a number
     *
     * In JSON value, if it starts with demical digits, it means next serveral
     * characters may reprent a number. According to mappings before, them
     * should be parsed to a Java long or double.
     * @return
     */
    private Object parseToNumber() {
        StringBuilder sb = new StringBuilder();
        sb.append(json.current());
        boolean isLong = true;
        while (json.hasNext()) {
            switch (json.next()) {
                case '0':
                case '1':
                case '2':
                case '3':
                case '4':
                case '5':
                case '6':
                case '7':
                case '8':
                case '9':
                    sb.append(json.current());
                    break;
                case '.':
                case 'e':
                case 'E':
                    sb.append(json.current());
                    isLong = false;
                    break;
                default:
                    if (isLong) {
                        return Long.parseLong(sb.toString());
                    } else {
                        return Double.parseDouble(sb.toString());
                    }
            }
        }
        return null;
    }

    /**
     * A wrapper class of JSON string. It supports some simple and usefull
     * methods, which are used in JSONParser class.
     */
    class JSONString {

        private String json;
        private int index;

        public JSONString(String json) {
            this.json = json;
            this.index = -1;
        }

        /**
         * Get current index of JSON string
         *
         * @return
         */
        public int getIndex() {
            return index;
        }

        /**
         * Get current character of JSON string
         * @return
         */
        public char current() {
            return index == -1 ? next() : json.charAt(index);
        }

        /**
         * Determine whether JSON string has next character or not
         * @return
         */
        public boolean hasNext() {
            return (index + 1) < json.length();
        }

        /**
         * Get a character following the current one
         * @return
         */
        public char next() {
            return json.charAt(++index);
        }

        /**
         * Get the rest of JSON string from current character
         * @return
         */
        public String sub() {
            return json.substring(index);
        }

        /**
         * Move the index back by one
         */
        private void back() {
            index--;
        }
    }
}
